#region References
using Server.Commands;
using Server.Commands.Generic;
using Server.Gumps;
using Server.Mobiles;
using Server.Targeting;
using System;
using System.Reflection;
using CPA = Server.CommandPropertyAttribute;
#endregion

namespace Server.Commands
{
    [Flags]
    public enum PropertyAccess
    {
        Read = 0x01,
        Write = 0x02,
        ReadWrite = Read | Write
    }

    public class Properties
    {
        private static readonly Type _TypeOfCPA = typeof(CPA);
        private static readonly Type _TypeOfSerial = typeof(Serial);
        private static readonly Type _TypeOfType = typeof(Type);
        private static readonly Type _TypeOfChar = typeof(char);
        private static readonly Type _TypeOfString = typeof(string);
        private static readonly Type _TypeOfIDynamicEnum = typeof(IDynamicEnum);
        private static readonly Type _TypeOfText = typeof(TextDefinition);
        private static readonly Type _TypeOfTimeSpan = typeof(TimeSpan);
        private static readonly Type _TypeOfParsable = typeof(ParsableAttribute);

        private static readonly Type[] _ParseTypes = new[] { typeof(string) };
        private static readonly object[] _ParseParams = new object[1];

        private static readonly Type[] _NumericTypes = new[]
        {
            typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long),
            typeof(ulong)
        };

        public static void Initialize()
        {
            CommandSystem.Register("Props", AccessLevel.Counselor, Props_OnCommand);
        }

        public static CPA GetCPA(PropertyInfo p)
        {
            object[] attrs = p.GetCustomAttributes(_TypeOfCPA, false);

            return attrs.Length == 0 ? null : attrs[0] as CPA;
        }

        public static PropertyInfo[] GetPropertyInfoChain(
            Mobile m, Type type, string propertyString, PropertyAccess endAccess, ref string failReason)
        {
            string[] split = propertyString.Split('.');

            if (split.Length == 0)
            {
                return null;
            }

            PropertyInfo[] info = new PropertyInfo[split.Length];

            for (int i = 0; i < info.Length; ++i)
            {
                string propertyName = split[i];

                if (CIEqual(propertyName, "current"))
                {
                    continue;
                }

                PropertyInfo[] props = type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public);

                bool isFinal = i == info.Length - 1;

                PropertyAccess access = endAccess;

                if (!isFinal)
                {
                    access |= PropertyAccess.Read;
                }

                foreach (PropertyInfo p in props)
                {
                    if (!CIEqual(p.Name, propertyName))
                    {
                        continue;
                    }

                    CPA attr = GetCPA(p);

                    if (attr == null)
                    {
                        failReason = string.Format("Property '{0}' not found.", propertyName);
                        return null;
                    }

                    if ((access & PropertyAccess.Read) != 0 && m.AccessLevel < attr.ReadLevel)
                    {
                        failReason = string.Format(
                            "You must be at least {0} to get the property '{1}'.", Mobile.GetAccessLevelName(attr.ReadLevel), propertyName);

                        return null;
                    }

                    if ((access & PropertyAccess.Write) != 0 && m.AccessLevel < attr.WriteLevel)
                    {
                        failReason = string.Format(
                            "You must be at least {0} to set the property '{1}'.", Mobile.GetAccessLevelName(attr.WriteLevel), propertyName);

                        return null;
                    }

                    if ((access & PropertyAccess.Read) != 0 && !p.CanRead)
                    {
                        failReason = string.Format("Property '{0}' is write only.", propertyName);
                        return null;
                    }

                    if ((access & PropertyAccess.Write) != 0 && (!p.CanWrite || attr.ReadOnly) && isFinal)
                    {
                        failReason = string.Format("Property '{0}' is read only.", propertyName);
                        return null;
                    }

                    info[i] = p;
                    type = p.PropertyType;
                    break;
                }

                if (info[i] != null)
                {
                    continue;
                }

                failReason = string.Format("Property '{0}' not found.", propertyName);
                return null;
            }

            return info;
        }

        public static PropertyInfo GetPropertyInfo(
            Mobile m, ref object obj, string propertyName, PropertyAccess access, ref string failReason)
        {
            PropertyInfo[] chain = GetPropertyInfoChain(m, obj.GetType(), propertyName, access, ref failReason);

            return chain == null ? null : GetPropertyInfo(ref obj, chain, ref failReason);
        }

        public static PropertyInfo GetPropertyInfo(ref object obj, PropertyInfo[] chain, ref string failReason)
        {
            if (chain == null || chain.Length == 0)
            {
                failReason = "Property chain is empty.";
                return null;
            }

            for (int i = 0; i < chain.Length - 1; ++i)
            {
                if (chain[i] == null)
                {
                    continue;
                }

                obj = chain[i].GetValue(obj, null);

                if (obj != null)
                {
                    continue;
                }

                failReason = string.Format("Property '{0}' is null.", chain[i]);
                return null;
            }

            return chain[chain.Length - 1];
        }

        public static string GetValue(Mobile from, object o, string name)
        {
            string failReason = "";

            PropertyInfo[] chain = GetPropertyInfoChain(from, o.GetType(), name, PropertyAccess.Read, ref failReason);

            if (chain == null || chain.Length == 0)
            {
                return failReason;
            }

            PropertyInfo p = GetPropertyInfo(ref o, chain, ref failReason);

            return p == null ? failReason : InternalGetValue(o, p, chain);
        }

        public static string IncreaseValue(Mobile m, object o, string[] args)
        {
            int len = args.Length / 2;

            object[] realObjs = new object[len];
            PropertyInfo[] realProps = new PropertyInfo[len];
            int[] realValues = new int[len];

            bool positive = false, negative = false;

            for (int i = 0; i < realProps.Length; ++i)
            {
                int idx = i * 2;
                string name = args[idx];

                try
                {
                    string valueString = args[1 + idx];

                    if (valueString.StartsWith("0x"))
                    {
                        realValues[i] = Convert.ToInt32(valueString.Substring(2), 16);
                    }
                    else
                    {
                        realValues[i] = Convert.ToInt32(valueString);
                    }
                }
                catch
                {
                    return "Offset value could not be parsed.";
                }

                if (realValues[i] > 0)
                {
                    positive = true;
                }
                else if (realValues[i] < 0)
                {
                    negative = true;
                }
                else
                {
                    return "Zero is not a valid value to offset.";
                }

                string failReason = null;

                realObjs[i] = o;
                realProps[i] = GetPropertyInfo(m, ref realObjs[i], name, PropertyAccess.ReadWrite, ref failReason);

                if (failReason != null)
                {
                    return failReason;
                }

                if (realProps[i] == null)
                {
                    return "Property not found.";
                }
            }

            for (int i = 0; i < realProps.Length; ++i)
            {
                object obj = realProps[i].GetValue(realObjs[i], null);

                if (!(obj is IConvertible))
                {
                    return "Property is not IConvertable.";
                }

                try
                {
                    long v = Convert.ToInt64(obj) + realValues[i];
                    object toSet = Convert.ChangeType(v, realProps[i].PropertyType);
                    realProps[i].SetValue(realObjs[i], toSet, null);

                    EventSink.InvokeOnPropertyChanged(new OnPropertyChangedEventArgs(m, realObjs[i], realProps[i], obj, toSet));
                }
                catch
                {
                    return "Value could not be converted";
                }
            }

            if (realProps.Length == 1)
            {
                return string.Format("The property has been {0}.", positive ? "increased." : "decreased");
            }

            if (positive || negative)
            {
                return "The properties have been changed.";
            }

            return string.Format("The properties have been {0}.", positive ? "increased." : "decreased");
        }

        public static string SetValue(Mobile m, object o, string name, string value)
        {
            object logObject = o;
            string failReason = "";
            PropertyInfo p = GetPropertyInfo(m, ref o, name, PropertyAccess.Write, ref failReason);

            return p == null ? failReason : InternalSetValue(m, logObject, o, p, name, value, true);
        }

        public static string ConstructFromString(Type type, object obj, string value, ref object constructed)
        {
            object toSet;
            bool isSerial = IsSerial(type);

            if (isSerial) // mutate into int32
            {
                type = _NumericTypes[4];
            }

            if (value == "(-null-)" && !type.IsValueType)
            {
                value = null;
            }

            if (IsEnum(type))
            {
                try
                {
                    toSet = Enum.Parse(type, value ?? string.Empty, true);
                }
                catch
                {
                    return "That is not a valid enumeration member.";
                }
            }
            else if (IsType(type))
            {
                try
                {
                    toSet = ScriptCompiler.FindTypeByName(value);

                    if (toSet == null)
                    {
                        return "No type with that name was found.";
                    }
                }
                catch
                {
                    return "No type with that name was found.";
                }
            }
            else if (IsParsable(type))
            {
                try
                {
                    toSet = Parse(obj, type, value);
                }
                catch
                {
                    return "That is not properly formatted.";
                }
            }
            else if (value == null)
            {
                toSet = null;
            }
            else if (value.StartsWith("0x") && IsNumeric(type))
            {
                try
                {
                    toSet = Convert.ChangeType(Convert.ToUInt64(value.Substring(2), 16), type);
                }
                catch
                {
                    return "That is not properly formatted.";
                }
            }
            else
            {
                try
                {
                    toSet = Convert.ChangeType(value, type);
                }
                catch
                {
                    return "That is not properly formatted.";
                }
            }

            if (isSerial) // mutate back
            {
                toSet = (Serial)Convert.ToInt32(toSet);
            }

            constructed = toSet;
            return null;
        }

        public static string SetDirect(
            Mobile m, object logObject, object obj, PropertyInfo prop, string givenName, object toSet, bool shouldLog)
        {
            try
            {
                if (toSet is AccessLevel)
                {
                    AccessLevel newLevel = (AccessLevel)toSet;
                    AccessLevel reqLevel = AccessLevel.Administrator;

                    if (newLevel == AccessLevel.Administrator)
                    {
                        reqLevel = AccessLevel.Developer;
                    }
                    else if (newLevel >= AccessLevel.Developer)
                    {
                        reqLevel = AccessLevel.Owner;
                    }

                    if (m.AccessLevel < reqLevel)
                    {
                        return "You do not have access to that level.";
                    }
                }

                if (shouldLog)
                {
                    CommandLogging.LogChangeProperty(m, logObject, givenName, toSet == null ? "(-null-)" : toSet.ToString());
                }

                if (obj is PlayerMobile && prop.PropertyType == typeof(Map) && toSet == null)
                {
                    return "Invalid -null- value, propery not set.";
                }

                object oldValue = prop.GetValue(obj, null);
                prop.SetValue(obj, toSet, null);

                EventSink.InvokeOnPropertyChanged(new OnPropertyChangedEventArgs(m, obj, prop, oldValue, toSet));

                return "Property has been set.";
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                return "An exception was caught, the property may not be set.";
            }
        }

        public static string SetDirect(object obj, PropertyInfo prop, object toSet)
        {
            try
            {
                if (toSet is AccessLevel)
                {
                    return "You do not have access to that level.";
                }

                if (obj is PlayerMobile && prop.PropertyType == typeof(Map) && toSet == null)
                {
                    return "Invalid -null- value, propery not set.";
                }

                object oldValue = prop.GetValue(obj, null);
                prop.SetValue(obj, toSet, null);

                EventSink.InvokeOnPropertyChanged(new OnPropertyChangedEventArgs(null, obj, prop, oldValue, toSet));

                return "Property has been set.";
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                return "An exception was caught, the property may not be set.";
            }
        }

        public static string InternalSetValue(
            Mobile m, object logobj, object o, PropertyInfo p, string pname, string value, bool shouldLog)
        {
            IDynamicEnum toSet;
            object toSetObj = null;
            string result = null;

            if (IsIDynamicEnum(p.PropertyType))
            {
                toSetObj = toSet = p.GetValue(o, null) as IDynamicEnum;

                if (toSet != null)
                {
                    toSet.Value = value;

                    if (!toSet.IsValid)
                    {
                        result = "No type with that name was found.";
                    }
                }
                else
                {
                    result = "That is not properly formatted.";
                }
            }
            else
            {
                result = ConstructFromString(p.PropertyType, o, value, ref toSetObj);
            }

            return result ?? SetDirect(m, logobj, o, p, pname, toSetObj, shouldLog);
        }

        public static string InternalSetValue(object o, PropertyInfo p, string value)
        {
            IDynamicEnum toSet;
            object toSetObj = null;
            string result = null;

            if (IsIDynamicEnum(p.PropertyType))
            {
                toSetObj = toSet = p.GetValue(o, null) as IDynamicEnum;

                if (toSet != null)
                {
                    toSet.Value = value;

                    if (!toSet.IsValid)
                    {
                        result = "No type with that name was found.";
                    }
                }
                else
                {
                    result = "That is not properly formatted.";
                }
            }
            else
            {
                result = ConstructFromString(p.PropertyType, o, value, ref toSetObj);
            }

            return result ?? SetDirect(o, p, toSetObj);
        }

        [Usage("Props [serial]")]
        [Description("Opens a menu where you can view and edit all properties of a targeted (or specified) object.")]
        private static void Props_OnCommand(CommandEventArgs e)
        {
            if (e.Length == 1)
            {
                IEntity ent = World.FindEntity(e.GetInt32(0));

                if (ent == null)
                {
                    e.Mobile.SendMessage("No object with that serial was found.");
                }
                else if (!BaseCommand.IsAccessible(e.Mobile, ent))
                {
                    e.Mobile.SendMessage("That is not accessible.");
                }
                else
                {
                    e.Mobile.SendGump(new PropertiesGump(e.Mobile, ent));
                }
            }
            else
            {
                e.Mobile.Target = new PropsTarget();
            }
        }

        private static bool CIEqual(string l, string r)
        {
            return Insensitive.Equals(l, r);
        }

        private static string InternalGetValue(object o, PropertyInfo p, PropertyInfo[] chain = null)
        {
            Type type = p.PropertyType;
            object value = p.GetValue(o, null);
            string toString;

            if (value == null)
            {
                toString = "null";
            }
            else if (IsNumeric(type))
            {
                toString = string.Format("{0} (0x{0:X})", value);
            }
            else if (IsChar(type))
            {
                toString = string.Format("'{0}' ({1} [0x{1:X}])", value, (int)value);
            }
            else if (IsString(type))
            {
                toString = (string)value == "null" ? @"@""null""" : string.Format("\"{0}\"", value);
            }
            else if (IsIDynamicEnum(type))
            {
                toString = ((IDynamicEnum)value).Value == "null"
                               ? @"@""null"""
                               : string.Format("\"{0}\"", ((IDynamicEnum)value).Value);
            }
            else if (IsText(type))
            {
                toString = ((TextDefinition)value).Format(false);
            }
            else
            {
                toString = value.ToString();
            }

            if (chain == null)
            {
                return string.Format("{0} = {1}", p.Name, toString);
            }

            string[] concat = new string[chain.Length * 2 + 1];

            for (int i = 0; i < chain.Length; ++i)
            {
                int idx = i * 2;

                concat[idx] = chain[i].Name;
                concat[idx + 1] = i < chain.Length - 1 ? "." : " = ";
            }

            concat[concat.Length - 1] = toString;

            return string.Concat(concat);
        }

        private static bool IsSerial(Type t)
        {
            return t == _TypeOfSerial;
        }

        private static bool IsType(Type t)
        {
            return t == _TypeOfType;
        }

        private static bool IsChar(Type t)
        {
            return t == _TypeOfChar;
        }

        private static bool IsString(Type t)
        {
            return t == _TypeOfString;
        }

        private static bool IsIDynamicEnum(Type t)
        {
            return _TypeOfIDynamicEnum.IsAssignableFrom(t);
        }

        private static bool IsText(Type t)
        {
            return t == _TypeOfText;
        }

        private static bool IsEnum(Type t)
        {
            return t.IsEnum;
        }

        private static bool IsParsable(Type t)
        {
            return t == _TypeOfTimeSpan || t.IsDefined(_TypeOfParsable, false);
        }

        private static object Parse(object o, Type t, string value)
        {
            MethodInfo method = t.GetMethod("Parse", _ParseTypes);

            _ParseParams[0] = value;

            return method.Invoke(o, _ParseParams);
        }

        private static bool IsNumeric(Type t)
        {
            return Array.IndexOf(_NumericTypes, t) >= 0;
        }

        private class PropsTarget : Target
        {
            public PropsTarget()
                : base(-1, true, TargetFlags.None)
            { }

            protected override void OnTarget(Mobile from, object o)
            {
                if (!BaseCommand.IsAccessible(from, o))
                {
                    from.SendMessage("That is not accessible.");
                }
                else
                {
                    from.SendGump(new PropertiesGump(from, o));
                }
            }
        }
    }
}

namespace Server
{
    public abstract class PropertyException : ApplicationException
    {
        public Property Property { get; protected set; }

        public PropertyException(Property property, string message)
            : base(message)
        {
            Property = property;
        }
    }

    public abstract class BindingException : PropertyException
    {
        public BindingException(Property property, string message)
            : base(property, message)
        { }
    }

    public sealed class NotYetBoundException : BindingException
    {
        public NotYetBoundException(Property property)
            : base(property, string.Format("Property has not yet been bound."))
        { }
    }

    public sealed class AlreadyBoundException : BindingException
    {
        public AlreadyBoundException(Property property)
            : base(property, string.Format("Property has already been bound."))
        { }
    }

    public sealed class UnknownPropertyException : BindingException
    {
        public UnknownPropertyException(Property property, string current)
            : base(property, string.Format("Property '{0}' not found.", current))
        { }
    }

    public sealed class ReadOnlyException : BindingException
    {
        public ReadOnlyException(Property property)
            : base(property, "Property is read-only.")
        { }
    }

    public sealed class WriteOnlyException : BindingException
    {
        public WriteOnlyException(Property property)
            : base(property, "Property is write-only.")
        { }
    }

    public abstract class AccessException : PropertyException
    {
        public AccessException(Property property, string message)
            : base(property, message)
        { }
    }

    public sealed class InternalAccessException : AccessException
    {
        public InternalAccessException(Property property)
            : base(property, "Property is internal.")
        { }
    }

    public abstract class ClearanceException : AccessException
    {
        public AccessLevel PlayerAccess { get; protected set; }
        public AccessLevel NeededAccess { get; protected set; }

        public ClearanceException(Property property, AccessLevel playerAccess, AccessLevel neededAccess, string accessType)
            : base(
                property,
                string.Format(
                    "You must be at least {0} to {1} this property, you are currently {2}.",
                    Mobile.GetAccessLevelName(neededAccess),
                    accessType,
                    Mobile.GetAccessLevelName(playerAccess)))
        { }
    }

    public sealed class ReadAccessException : ClearanceException
    {
        public ReadAccessException(Property property, AccessLevel playerAccess, AccessLevel neededAccess)
            : base(property, playerAccess, neededAccess, "read")
        { }
    }

    public sealed class WriteAccessException : ClearanceException
    {
        public WriteAccessException(Property property, AccessLevel playerAccess, AccessLevel neededAccess)
            : base(property, playerAccess, neededAccess, "write")
        { }
    }

    public sealed class Property
    {
        private PropertyInfo[] _Chain;

        public PropertyAccess Access { get; private set; }
        public string Binding { get; private set; }

        public bool IsBound => _Chain != null;

        public PropertyInfo[] Chain
        {
            get
            {
                if (!IsBound)
                {
                    throw new NotYetBoundException(this);
                }

                return _Chain;
            }
        }

        public Type Type
        {
            get
            {
                if (!IsBound)
                {
                    throw new NotYetBoundException(this);
                }

                return _Chain[_Chain.Length - 1].PropertyType;
            }
        }

        public Property(string binding)
        {
            Binding = binding;
        }

        public Property(PropertyInfo[] chain)
        {
            _Chain = chain;
        }

        public static Property Parse(Type type, string binding, PropertyAccess access)
        {
            Property prop = new Property(binding);

            prop.BindTo(type, access);

            return prop;
        }

        public bool CheckAccess(Mobile from)
        {
            if (!IsBound)
            {
                throw new NotYetBoundException(this);
            }

            for (int i = 0; i < _Chain.Length; ++i)
            {
                PropertyAccess access = Access;
                PropertyInfo prop = _Chain[i];
                bool isFinal = i == _Chain.Length - 1;

                if (!isFinal)
                {
                    access |= PropertyAccess.Read;
                }

                CPA security = Properties.GetCPA(prop);

                if (security == null)
                {
                    throw new InternalAccessException(this);
                }

                if ((access & PropertyAccess.Read) != 0 && from.AccessLevel < security.ReadLevel)
                {
                    throw new ReadAccessException(this, from.AccessLevel, security.ReadLevel);
                }

                if ((access & PropertyAccess.Write) != 0 && (from.AccessLevel < security.WriteLevel || security.ReadOnly))
                {
                    throw new WriteAccessException(this, from.AccessLevel, security.ReadLevel);
                }
            }

            return true;
        }

        public void BindTo(Type objectType, PropertyAccess desiredAccess)
        {
            if (IsBound)
            {
                throw new AlreadyBoundException(this);
            }

            string[] split = Binding.Split('.');
            PropertyInfo[] chain = new PropertyInfo[split.Length];

            for (int i = 0; i < split.Length; ++i)
            {
                bool isFinal = i == chain.Length - 1;

                chain[i] = objectType.GetProperty(split[i], BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);

                if (chain[i] == null)
                {
                    throw new UnknownPropertyException(this, split[i]);
                }

                objectType = chain[i].PropertyType;

                PropertyAccess access = desiredAccess;

                if (!isFinal)
                {
                    access |= PropertyAccess.Read;
                }

                if ((access & PropertyAccess.Read) != 0 && !chain[i].CanRead)
                {
                    throw new WriteOnlyException(this);
                }

                if ((access & PropertyAccess.Write) != 0 && !chain[i].CanWrite)
                {
                    throw new ReadOnlyException(this);
                }
            }

            Access = desiredAccess;
            _Chain = chain;
        }

        public override string ToString()
        {
            if (!IsBound)
            {
                return Binding;
            }

            string[] toJoin = new string[_Chain.Length];

            for (int i = 0; i < toJoin.Length; ++i)
            {
                toJoin[i] = _Chain[i].Name;
            }

            return string.Join(".", toJoin);
        }
    }
}
